// Copyright 2010 Alexandre Tristan St-Cyr <tristan.stcyr@gmail.com>

package com.codeplex.loadjava;

import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import org.objectweb.asm.Opcodes;

import com.codeplex.loadjava.model.ClassDefinition;
import com.codeplex.loadjava.model.Field;
import com.codeplex.loadjava.model.FieldDefinition;
import com.codeplex.loadjava.model.FieldReference;
import com.codeplex.loadjava.model.InterfaceImplementation;
import com.codeplex.loadjava.model.JavaClass;
import com.codeplex.loadjava.model.Method;
import com.codeplex.loadjava.model.MethodDefinition;
import com.codeplex.loadjava.model.MethodException;
import com.codeplex.loadjava.model.MethodInvocation;
import com.codeplex.loadjava.model.MethodParameter;
import com.codeplex.loadjava.model.TypeReference;


class JavaModelLoaderVisitor implements IJavaModelVisitor
{
	Logger logger = Logger.getLogger(JavaModelLoaderVisitor.class.getCanonicalName());
	
	private final IJavaRepository repository;

	private Map<JavaClass, Long> classIds = new HashMap<JavaClass, Long>();
	
	private Map<Method, Long> methodIds = new HashMap<Method, Long>();
	
	private Map<TypeReference, Long> typeIds = new HashMap<TypeReference, Long>();
	
	private Map<Field, Long> fieldIds = new HashMap<Field, Long>();
	
	private Long nextClassId, nextMethodId, nextTypeId, nextFieldId;
	
	JavaModelLoaderVisitor(
			IJavaRepository repository,
			Long firstClassId, 
			Long firstMethodId, 
			Long firstTypeId, 
			Long firstFieldId) {
		this.repository = repository;
		this.nextClassId = firstClassId;
		this.nextMethodId = firstMethodId;
		this.nextTypeId = firstTypeId;
		this.nextFieldId = firstFieldId;
	}
	
	private static int getAccessType(int access) {
		if ((access & Opcodes.ACC_PUBLIC) != 0)
			return IJavaRepository.ACC_PUBLIC;
		if ((access & Opcodes.ACC_PROTECTED) != 0)
			return IJavaRepository.ACC_PROTECTED;
		if ((access & Opcodes.ACC_PRIVATE) != 0)
			return IJavaRepository.ACC_PRIVATE;
		return IJavaRepository.ACC_DEFAULT;
	}

	private long getTypeId(TypeReference type)
	{
		Long id = this.typeIds.get(type);
		if (id == null) {
			throw new JavaLoaderException("Type "+type.getDescriptor()+ 
					" was referenced before being allocated an id." );
		}
		return id;
	}
	
	private long getClassId(JavaClass javaClass)
	{
		Long id = this.classIds.get(javaClass);
		if (id == null) {
			throw new JavaLoaderException("Class "+javaClass.getInternalName()+ 
					" was referenced before being allocated an id." );
		}
		return id;
	}
	
	private long getFieldId(Field field) {
		Long id = this.fieldIds.get(field);
		if (id == null) {
			throw new JavaLoaderException("Field referenced before being allocated an id");
		}
		return id;
	}
	
	private long getMethodId(Method method) {
		Long id = this.methodIds.get(method);
		if (id == null) {
			throw new JavaLoaderException("Method referenced before being allocated an id");
		}
		return id;
	}
	
	@Override
	public void visitClassFile(String relativeName) {
		//logger.log(Level.INFO, "Loading artifact \""+relativeName+"\"...");
	}

	@Override
	public void endVisitClassFile(String relativeName) {
		//logger.log(Level.INFO, "Done loading artifact \""+relativeName+"\"");
	}

	@Override
	public void visitField(Field field) {
		try {
			long id = this.nextFieldId++;
			this.fieldIds.put(field, id);
			repository.insertField(id, this.getClassId(field.getDeclaringClass()), 
					field.getName(), this.getTypeId(field.getType()));
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void visitMethod(Method method) {
		long id = nextMethodId++;
		this.methodIds.put(method, id);
		try {
			repository.insertMethod(id, method.getName(), 
					method.getDescriptor(), 
					this.getClassId(method.getDeclaringClass()));
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void visitClass(JavaClass javaClass) {
		long id = nextClassId++;
		this.classIds.put(javaClass, id);
		try {
			repository.insertClass(id, javaClass.getInternalName());
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void visitType(TypeReference typeReference) {
		long id = nextTypeId++;
		this.typeIds.put(typeReference, id);
		try {
			JavaClass typeClass = typeReference.getJavaClass();
			Long classId = typeClass != null ? this.getClassId(typeClass) : null;
			this.repository.insertType(id, 
					typeReference.getTypeKind(), 
					typeReference.getArrayDimensions(), 
					classId, 
					typeReference.getType().getDescriptor());
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void visitFieldDefinition(FieldDefinition fieldDefinition) {
		try
		{	
			repository.insertFieldDefinition(
					this.getFieldId(fieldDefinition),
					fieldDefinition.getName(), 
					fieldDefinition.getSignature(), 
					this.getTypeId(fieldDefinition.getType()),
					getAccessType(fieldDefinition.getAccess()),
					fieldDefinition.isStatic(),
					fieldDefinition.isFinal(),
					fieldDefinition.isVolatile(),
					fieldDefinition.isTransient());
					
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void visitMethodDefinition(MethodDefinition methodDefinition) {
		long methodId = this.getMethodId(methodDefinition);
		try {
			this.repository.insertMethodDefinition(
					methodId, 
					getAccessType(methodDefinition.getAccess()),
					methodDefinition.isSynchronized(),
					methodDefinition.isNative(),
					methodDefinition.isAbstract(),
					methodDefinition.isStrict(),
					methodDefinition.getSignature());
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void visitMethodException(MethodException methodException) {
		// TODO: Implement when method exception methods is added
	}

	@Override
	public void visitClassDefinition(ClassDefinition classDefinition) {
		try {
			JavaClass outerClass = classDefinition.getOuterClass();
			
			repository.insertClassDefinition(
					this.getClassId(classDefinition),
					classDefinition.getSignature(), 
					classDefinition.isInterface(), 
					classDefinition.isAbstract(), 
					classDefinition.isFinal(),
					classDefinition.isPublic(),
					classDefinition.isSuper(),
					classDefinition.isAnnotation(),
					classDefinition.isEnum(),
					classDefinition.isSynthetic(),
					this.getClassId(classDefinition), 
					outerClass == null ? null : this.getClassId(outerClass), 
					classDefinition.getPackageName());
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void visitFieldReference(FieldReference fieldReference) {
		long fieldId = getFieldId(fieldReference.getField());
		long methodId = getMethodId(fieldReference.getMethod());
		try {
			repository.insertFieldReference(methodId, fieldId);
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void visitMethodInvocation(MethodInvocation methodInvocation) {
		try {
			this.repository.insertMethodInvocation(
					this.getMethodId(methodInvocation.getCaller()), 
					this.getMethodId(methodInvocation.getCallee()));
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void visitInterfaceImplementations(InterfaceImplementation interfaceImplementation) {
		try
		{
			long implementationId = this.getClassId(interfaceImplementation.getImplementationClass());
			long interfaceId = this.getClassId(interfaceImplementation.getInterfaceClass());
			this.repository.insertInterfaceImplementation(implementationId, interfaceId);
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void visitMethodParameter(MethodParameter parameter) {
		long methodId = getMethodId(parameter.getMethodDefinition());
		long typeId = this.getTypeId(parameter.getType());
		try {
			this.repository.insertMethodParameter(
					parameter.getIndex(), methodId, typeId);
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}	
	}
}